﻿using System.Collections.Generic;
using DeepEarth.Core;
using DeepEarth.Toolkit.Controls.Measure;
using System.Linq;
using System;
using System.Windows;
using DeepEarth.Toolkit.Geometry;
using DeepEarth.Map.Core.Utilities;
namespace DeepEarth.Toolkit.Controls
{
    public class AreaCalculator
    {
        public double MaxAngularDistance = 0.02;

        // these are used to determine intersections - just need some functions that can convert lat lon to XY in a uniform way
        Func<Point, Location> XYToLocation;
        Func<Location, Point> LocationToXY;

        public AreaCalculator(Func<Location, Point> locationToXY, Func<Point, Location> xyToLocation)
        {
            this.LocationToXY = locationToXY;
            this.XYToLocation = xyToLocation;
        }

        // If custom functions aren't provided, can't calculate area where a line crosses the dateline because this uses Logical Coordinates
        public AreaCalculator()
        {           
            this.LocationToXY = CoordinateTransformation.GeographicToLogical;
            this.XYToLocation = CoordinateTransformation.LogicalToGeographic;
        }

        // Returns the area of the polygon described by an ordered set of locations.
        public double Calculate(List<Location> incomingLocations, bool interpolate)
        {
            if (incomingLocations.Count < 3)
            {
                return 0;
            }

            List<Location> locations = new List<Location>();

            foreach (Location l in incomingLocations)
            {
                locations.Add(l.Clone());
            }

            if (locations[0] != locations[locations.Count - 1])
            {
                locations.Add(locations[0].Clone());
            }

            try
            {
                List<SimplePolygon> polygons = SimplePolygon.Simplify(locations, this.LocationToXY, this.XYToLocation);

                // Simplify can return null if the polygon is too complex.
                if (polygons == null || !polygons.Any())
                {
                    return double.NaN;
                }

                //check polygons
                foreach (var simplePolygon in polygons)
                {
                    var vertices = simplePolygon.GetVertices();
                    System.Diagnostics.Debug.Assert(vertices.Count >= 3);
                    System.Diagnostics.Debug.Assert(vertices[0] == vertices[vertices.Count - 1]);
                }

                polygons = Zone(polygons);

                double area = 0;

                foreach (SimplePolygon p in polygons)
                {
                    area += CalcuateArea(p, interpolate);
                }

                return area;
            }
            catch 
            {
                return double.NaN;
            }

        }


        // Separate into UTM zones
        private List<SimplePolygon> Zone(List<SimplePolygon> polygons)
        {
            List<SimplePolygon> result = new List<SimplePolygon>();
            foreach (var sp in polygons)
            {
                var vertices = sp.GetVertices();
                var simplified = Zone(vertices);

                result = result.Concat(simplified).ToList();
            }

            return result;
        }

        private List<SimplePolygon> Zone(List<Location> locations)
        {
            SimplePolygon initial = new SimplePolygon(locations);
            List<SimplePolygon> potentials = new List<SimplePolygon>();
            potentials.Add(initial);

            LocationRectangle boundingBox = new LocationRectangle(locations);

            if (boundingBox.North > 0 && boundingBox.South < 0)
            {
                potentials.Remove(initial);
                var northandsouth = initial.SpliceAtEquator();

                List<SimplePolygon> northern = SimplePolygon.FindCircuits(northandsouth.Item1);
                List<SimplePolygon> southern = SimplePolygon.FindCircuits(northandsouth.Item2);

                potentials = northern.Concat(southern).ToList();

            }

            // create a list of utm zone boundaries
            List<double> zoneBoundaries = new List<double>();

            UTMLocation northWest = AreaHelper.ConvertToUTM(boundingBox.NorthWest);
            UTMLocation southEast = AreaHelper.ConvertToUTM(boundingBox.SouthEast);

            // check to see if the polygon crosses any zone boundaries
            // we want the eastern zone boundaries of all of the zones involved.
            // i.e. if the initial polygon is wholly in one zone, we still want that zone boundary
            if (southEast.Zone.Number - northWest.Zone.Number >= 0)
            {
                int currentZone = northWest.Zone.Number;

                while (currentZone <= southEast.Zone.Number)
                {
                    // get the longitude of the zone boundaries and add it to the zoneBoundaries collection
                    zoneBoundaries.Add(AreaHelper.ZoneEasternLongitude(currentZone));

                    currentZone++;
                }
            }

            List<SimplePolygon> polygons = new List<SimplePolygon>();

            // Slice the polygon along the zone boundaries
            foreach (double zoneBoundary in zoneBoundaries)
            {
                int potentialIndex = 0;
                while (potentialIndex < potentials.Count)
                {
                    SimplePolygon p = potentials[potentialIndex];

                    potentials.RemoveAt(potentialIndex);

                    // Split the polygon. Add edges corresponding to the zone boundary where appropriate
                    var leftAndRight = p.Splice(zoneBoundary);

                    // any polygons on the left of the boundary will be correctly zoned
                    var zonedPolygons = SimplePolygon.FindCircuits(leftAndRight.Item1);

                    if (zonedPolygons != null)
                    {
                        foreach (SimplePolygon zoned in zonedPolygons)
                        {
                            polygons.Add(zoned);
                        }
                    }

                    // anything on the right of the zone boundary may span multiple boundaries and will need to be re-evaluated.
                    var newPolygons = SimplePolygon.FindCircuits(leftAndRight.Item2);

                    if (newPolygons != null)
                    {
                        foreach (SimplePolygon remaining in newPolygons)
                        {
                            potentials.Insert(potentialIndex, remaining);
                        }

                        potentialIndex += newPolygons.Count;
                    }
                }
            }

            return polygons;
        }
        
        private double CalcuateArea(SimplePolygon p, bool interpolate)
        {
            List<Location> vertices = p.GetVertices();

            // Interpolate points for better accuracy for polygons in the mercator projection
            if (interpolate)
            {
                // TODO: Interpolate using screen coordinates. Interpolation based on angular distance doesn't help much.
                //vertices = InterpolatePoints(vertices, MaxAngularDistance);
            }

            return CalculateArea(vertices);
        }

        private double CalculateArea(List<Location> vertices)
        {
            // polygon should be in the one utm zone. Some vertices will be at zone boundaries.

            double maxLon = double.MinValue;

            foreach (Location l in vertices)
            {
                if (l.Longitude > maxLon)
                {
                    maxLon = l.Longitude;
                }
            }

            // make sure we covert to UTM based on one zone.            
            int zone = AreaHelper.Zone(maxLon);

            List<UTMLocation> locations = vertices.Select(l => AreaHelper.ConvertToUTM(l, zone)).ToList();

            double area = 0;
            for (int index = 0; index < locations.Count - 1; index++)
            {
                UTMLocation i = locations[index];
                UTMLocation j = locations[index + 1];

                area += i.Easting * j.Northing - i.Northing * j.Easting;
            }

            return Math.Abs(area / 2.0);
        }

        private List<Location> InterpolatePoints(List<Location> vertices, double maxAngularDistance)
        {
            int index = 0;

            while (index < vertices.Count - 1)
            {
                Location a = vertices[index];
                Location b = vertices[index + 1];

                Edge e = new Edge
                {
                    P1 = a,
                    P2 = b
                };

                double angularDistance = Math.Sqrt(Math.Pow(e.Horizontal, 2) + Math.Pow(e.Vertical, 2));

                if (angularDistance > maxAngularDistance)
                {
                    double segments = Math.Floor(angularDistance / maxAngularDistance);

                    double horizontalDelta = e.Horizontal / segments;
                    double verticalDelta = e.Vertical / segments;

                    double currentLon = a.Longitude + horizontalDelta;
                    double currentLat = a.Latitude + verticalDelta;

                    for (int i = 0; i < segments; i++ )
                    {
                        Location interpolated = new Location
                        {
                            Longitude = currentLon,
                            Latitude = currentLat
                        };

                        index++;
                        vertices.Insert(index, interpolated);

                        currentLon += horizontalDelta;
                        currentLat += verticalDelta;

                    }

                    if (segments == 0)
                    {
                        index++;
                    }
                }
                else
                {
                    index++;
                }
            }

            return vertices;
        }
    }
}
